{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2cdbd06d-fe22-4cf5-92b4-9437e891f3d3",
   "metadata": {},
   "source": [
    "# Python code to control USRP X310.\n",
    "\n",
    "From https://pysdr.org/content/usrp.html"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "434f7e9e-59c1-424c-b4f6-738d9145483c",
   "metadata": {},
   "source": [
    "To increase buffer size and maximum ethernet maximum transmit unit, run:\n",
    "\n",
    "```\n",
    "sudo sysctl -w net.core.wmem_max=24862979\n",
    "sudo ip link set dev enp47s0f0 mtu 9000  # or for whatever ethernet card\n",
    "sudo ip link set dev enp47s0f1 mtu 9000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "59449ded-1c78-4728-ba18-765add792fb0",
   "metadata": {},
   "source": [
    "# Spectrum and Recording: \"Empowered Worker Threads\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4393b4c7-2d84-447a-847b-8dffa990284e",
   "metadata": {},
   "source": [
    "* Lock the lock.\n",
    "* Look to see what others are doing -- either taking spectra or recording data\n",
    "* If Nthreads-1 others are recording data, I had better take spectra\n",
    "* Otherwise, mask out areas of the spectrum where others are taking data and look for the highest peak above a threshold.\n",
    "* If it's worth taking data there, do that.\n",
    "* Otherwise, pick a 1/Nthreads piece of spectra and take that.\n",
    "* Unlock the lock\n",
    "\n",
    "What data do we need?\n",
    "\n",
    "* List of what's currently being done:\n",
    "   - ('record', center_freq)\n",
    "   - ('spectrum', chunk_number)\n",
    "* Current spectra, along with how old each little NFFT-long segment is maybe so we can update the oldest spectral bins first\n",
    "\n",
    "If we are taking spectra and find a loud signal, should we instantly bail out and start recording? Yes, probably. And without gaps.\n",
    "\n",
    "If we are recording, when should we stop? After a minimal timeout or if the algorithm above says that something else is more interesting (record elsewhere or go back to taking spectra)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5354069f-048e-48ae-a129-5b1ac3c49621",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pylab inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ce086086-4ea6-4a7e-a278-de814423da4f",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time, datetime\n",
    "import threading\n",
    "import uhd\n",
    "import matplotlib.mlab  # for psd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e66b8d76-e0db-4d4a-9051-6aaec0cca172",
   "metadata": {},
   "outputs": [],
   "source": [
    "#threading.enumerate()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0cc09e4e-c734-4a83-b0e4-f5b52ac24ac1",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load the Bokeh stuff for jupyter notebooks\n",
    "from bokeh.io import push_notebook, show, output_notebook\n",
    "from bokeh.layouts import row\n",
    "from bokeh.plotting import figure\n",
    "from bokeh.models import ColumnDataSource\n",
    "import bokeh.palettes\n",
    "output_notebook()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b9e06ae0-6320-4183-96e6-9e86afdc8010",
   "metadata": {},
   "outputs": [],
   "source": [
    "# set up a dummy plot to be updated\n",
    "source = ColumnDataSource(\n",
    "    data={\n",
    "        \"freqs_MHz\":  [0],  # x\n",
    "        \"powers_dB\":  [0],  # y\n",
    "    }\n",
    ")\n",
    "source_marker = ColumnDataSource(\n",
    "    data={\n",
    "        \"marker_MHz\": [], # x\n",
    "        \"marker_dB\":  [],  # y\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab5de1db-d468-4a99-987f-e13fec62cb06",
   "metadata": {},
   "outputs": [],
   "source": [
    "p = figure(\n",
    "        height=400,\n",
    "        title='spectrum',\n",
    "        x_axis_label='freq [MHz]',\n",
    "        y_axis_label='power [dB]',\n",
    "        y_range=(-160, -60),\n",
    "        tools=(\"pan\", \"zoom_in\", \"zoom_out\", \"box_zoom\", \"wheel_zoom\", \"save\", \"reset\", \"help\", \"hover\"),\n",
    "        sizing_mode=\"stretch_width\",\n",
    "        tooltips=\"@freqs_MHz, @powers_dB\"  # define a tooltip using data from the x and y columns\n",
    "    )\n",
    "\n",
    "p.line(\n",
    "    x=\"freqs_MHz\",  # use the sequence in the \"x_values\" column\n",
    "    y=\"powers_dB\",  # use the sequence in the \"y_values\" column\n",
    "    source=source,  # use the ColumnDataSource as the data source\n",
    ")\n",
    "\n",
    "p.scatter(\n",
    "    x=\"marker_MHz\", \n",
    "    y=\"marker_dB\", \n",
    "    color='red',\n",
    "    source=source_marker,  # use the ColumnDataSource as the data source\n",
    ")\n",
    "\n",
    "# show the results\n",
    "handle = show(p, notebook_handle=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c754e105-4078-447f-9d1e-d013dd89c5d4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def find_most_interesting_thing_to_record(chan, spectrum_results, dB_threshold=-100):\n",
    "    'returns None if there is nothing interesting. NOT Thread safe. Called only by what_to_work_on_next()'\n",
    "    freqs = spectrum_results['freqs']\n",
    "    powers = spectrum_results['powers']\n",
    "    sample_rate= spectrum_results['sample_rate']\n",
    "    \n",
    "    freq_mask = np.ones(len(freqs), dtype=bool)  # start with all frequencies up for grabs\n",
    "    \n",
    "    # we'll only make test transmissions in a small frequency range, so ignore huge signals outside of that range\n",
    "    freq_mask *= ((freqs>=300e6)*(freqs<=330e6) +    # 4-button remote\n",
    "                  (freqs>=460e6)*(freqs<=470e6) +    # walkie talkies\n",
    "                  (freqs>=795e6)*(freqs<=1005e6) +   # 915 MHz B210 and drone telemetry radios\n",
    "                  (freqs>=2300e6)*(freqs<=2500e6) +  # WiFi and drone control\n",
    "                  (freqs>=5700e6)*(freqs<=5900e6)    # drone/car video\n",
    "                 ) \n",
    "    # now freq_mask contains 1s for interestnig frequencies 0s for uninteresting ones\n",
    "    \n",
    "    # mask out frequency ranges that others are already recording\n",
    "    NCHANS = len(spectrum_results['what_channel_is_working_on'])\n",
    "    for ch in range(NCHANS):\n",
    "        (task, center_freq) = spectrum_results['what_channel_is_working_on'][ch]\n",
    "        if ch!=chan and task=='record':\n",
    "            freq_mask *= (freqs<center_freq-sample_rate/2)+(freqs>=center_freq+sample_rate/2) # + is \"or\" for bools\n",
    "            \n",
    "    # now freq_mask contains 1s for potentially interesting signals and 0s for others\n",
    "    i = np.argmax(powers*freq_mask)\n",
    "    max_power_dB = 10*np.log10(powers[i])\n",
    "    center_freq = freqs[i]\n",
    "    if max_power_dB >= dB_threshold:\n",
    "        #print('maximum power at freq:', center_freq/1e6, 'MHz   power:', max_power_dB, 'dB' )\n",
    "        spectrum_results['trigger_power_dB'][center_freq] = max_power_dB  # record the power that triggered this for plotting.\n",
    "        return center_freq\n",
    "    else:\n",
    "        return None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8f1b8fd-c537-449a-a777-ab757cd99916",
   "metadata": {},
   "outputs": [],
   "source": [
    "def what_to_work_on_next(chan, spectrum_results):\n",
    "    '''\n",
    "    If everyone else is recording, I should take spectra no matter what.\n",
    "    Otherwise, I can look for something interesting to record.\n",
    "    If I should be taking spectra or find nothing to record, take spectra.\n",
    "\n",
    "    return either\n",
    "    newjob = ('recording', center_freq)\n",
    "    or\n",
    "    newjob = ('spectrum', i)  where i is 0..3, the part of the spectrum I should be recording\n",
    "    \n",
    "    This function is NOT thread safe and should be surrounded by \n",
    "    spectrum_lock.acquire()\n",
    "    and \n",
    "    spectrum_lock.release()\n",
    "    '''\n",
    "    # count how many OTHER channels are recording:\n",
    "    others_recording = 0\n",
    "    NCHANS = len(spectrum_results['what_channel_is_working_on'])\n",
    "    for ch in range(NCHANS):\n",
    "        (task, center_freq) = spectrum_results['what_channel_is_working_on'][ch]\n",
    "        if ch!=chan and task=='recording':\n",
    "            others_recording += 1\n",
    "            \n",
    "    if others_recording < NCHANS-1:\n",
    "        # Now I could look for something interesting to record\n",
    "        center_freq = find_most_interesting_thing_to_record(chan, spectrum_results)\n",
    "        if center_freq is not None:\n",
    "            newjob = ('record', center_freq)\n",
    "            spectrum_results['what_channel_is_working_on'][chan] = newjob\n",
    "            return newjob\n",
    "            \n",
    "    # if I made it here, I should take spectra\n",
    "    i = spectrum_results['next_spectral_range_index']\n",
    "    spectrum_results['next_spectral_range_index'] = (i+1)%NCHANS  # let the next thread take the next range\n",
    "    newjob = ('spectrum', i)\n",
    "    spectrum_results['what_channel_is_working_on'][chan] = newjob\n",
    "    return newjob"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a53e6ba2-f09f-402e-b9c4-eb7a302a3deb",
   "metadata": {},
   "outputs": [],
   "source": [
    "def empowered_rx_thread(usrp, streamer, chan, spectrum_results, spectrum_lock, quit_event):\n",
    "    \n",
    "    # For the setup and printing, let's get the lock.\n",
    "    spectrum_lock.acquire()\n",
    "    \n",
    "    #print('starting empowered_rx_thread')\n",
    "    #print(f'starting empowered_rx_thread usrp.get_rx_rate(chan={chan}):', usrp.get_rx_rate(chan=chan)/1e6, 'MHz')\n",
    "    #print('   spectrum_results.keys()', spectrum_results.keys())\n",
    "    \n",
    "    # update some global info that shouldn't change...\n",
    "    sample_rate = spectrum_results['sample_rate']\n",
    "    gain = spectrum_results['gain']  # right now, used only for recording filename\n",
    "    NFFT = spectrum_results['NFFT']  # sets number of spectral bins per tuning\n",
    "    center_freqs = spectrum_results['center_freqs']\n",
    "    #spectrum_lock.release()  # release here if you don't need to print any more.\n",
    "    \n",
    "    # setup some one-time constants and buffers that will get reused\n",
    "    #spectrum_Nchunks = 16  # More chunks for more accurate spectra (more averaging). Bin size is set by NFFT.\n",
    "    spectrum_Nchunks = 4  # More chunks for more accurate spectra (more averaging). Bin size is set by NFFT.  For bigger MTU\n",
    "    spectrum_num_samps = spectrum_Nchunks * streamer.get_max_num_samps()  # maybe it's nice to have a round number of max_num_samps\n",
    "    spectrum_recv_buffer = np.zeros((1, spectrum_num_samps), dtype=np.complex64)\n",
    "    #print(f'         empowered_rx_thread streamer.get_max_num_samps() for chan{chan}:', streamer.get_max_num_samps())\n",
    "    print(f'         empowered_rx_thread spectrum_num_samps for chan{chan}:', spectrum_num_samps, \n",
    "                     'or', spectrum_num_samps/sample_rate, 's')\n",
    "    spectrum_lock.release()  # TODO: remove and uncomment above\n",
    "    \n",
    "    record_time_seconds = 2.0\n",
    "    record_num_samps = int(np.ceil(record_time_seconds * sample_rate))\n",
    "    record_recv_buffer = np.zeros((1, record_num_samps), dtype=np.complex64)\n",
    "    \n",
    "    metadata = uhd.types.RXMetadata()\n",
    "    \n",
    "    while not quit_event.is_set():\n",
    "        spectrum_lock.acquire()\n",
    "        (task,value) = what_to_work_on_next(chan, spectrum_results)  # gives us a task, but also changes our status\n",
    "        #print(f'   empowered_rx_thread usrp.get_rx_rate(chan={chan}):', task, value)\n",
    "        spectrum_lock.release()\n",
    "        \n",
    "        if task=='spectrum':\n",
    "            # value is now 0 for the first 1/4 of the center_freqs, 1 for the second 1/4 etc.\n",
    "            # Use this value to calculate my_center_freqs\n",
    "            NCHANS = usrp.get_rx_num_channels()\n",
    "            NFREQS = len(center_freqs)//NCHANS  # number of center frequencies in each range\n",
    "            my_center_freqs = center_freqs[value*NFREQS:(value+1)*NFREQS]\n",
    "            \n",
    "            for i,center_freq in enumerate(my_center_freqs):\n",
    "                usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_freq), chan=chan)\n",
    "                while not usrp.get_rx_sensor('lo_locked', chan).value:\n",
    "                    # Hold on, this is literally never unlocked for X310! No need to even check and wait.\n",
    "                    time.sleep(0.0001)\n",
    "                    not_locked_count += 1\n",
    "                # Wait just for things to settle, but not long for X310. Longer seems to be cleaner.\n",
    "                #time.sleep(0.0001)  # This gives crazy shit in first bin for X310 TwinRX\n",
    "                #time.sleep(0.001)  # Even 1ms slows things down but not long enough to settle\n",
    "                time.sleep(0.002)  # 2ms is the shortest time that gives a clean 915 MHz signal. Not sure about the rest.\n",
    "                \n",
    "                # Start Spectrum Stream\n",
    "                stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) # stream num_samps and then done\n",
    "                #stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont) # stream continuous, so we can start recording right away\n",
    "                stream_cmd.stream_now = True\n",
    "                stream_cmd.num_samps = spectrum_num_samps\n",
    "                streamer.issue_stream_cmd(stream_cmd)  # often just stream num_samps\n",
    "                # Receive Samples\n",
    "                streamer.recv(spectrum_recv_buffer, metadata)  # writes into metadata\n",
    "                #if i==0:\n",
    "                #    print('chan', chan, 'center_freq', center_freq, 'spectrum_recv_buffer:', spectrum_recv_buffer[-4:])\n",
    "                # TODO: Also save metadata info\n",
    "                \n",
    "                #recv_stack[i,:] = recv_buffer[0]  # record these samples for this center frequency (slow for debug but maybe we save the trigger)\n",
    "                valid_buffer = spectrum_recv_buffer[len(spectrum_recv_buffer)//2,:]  # used to take second half, now we just wait longer\n",
    "                #valid_buffer = spectrum_recv_buffer[0,:]  # used to take second half, now we just wait longer\n",
    "                \n",
    "                # calculate PSD on these samples with a given bin spacing\n",
    "                # WARNING: You need to copy the buffer, as we do into valid_buffer. Otherwise PSD gives zero. I don't know why. A threading or shared memory issue.\n",
    "                psd_power, psd_frequencies = matplotlib.mlab.psd(\n",
    "                    valid_buffer, NFFT=NFFT, Fs=sample_rate, \n",
    "                    window=matplotlib.mlab.window_hanning, # window_none turns real spectral lines into wide volcanos due to discontinuity\n",
    "                    detrend=matplotlib.mlab.detrend_mean)  # detrend_none makes huge DC spike. detrend_linear might help or not but can't change after calib\n",
    "                #print('psd:', len(psd_power), len(psd_frequencies))\n",
    "                #assert(len(psd_power)==NFFT and len(psd_frequencies)==NFFT)  # True for complex samples\n",
    "                CHEAT = True\n",
    "                if CHEAT:\n",
    "                    # Cheat on the DC 0 offset bin by replacing the center-frequency with an average of the ones next to it\n",
    "                    psd_power[NFFT//2]  = (psd_power[NFFT//2-1] + psd_power[NFFT//2+1])/2.0  # CHEAT!\n",
    "                    # Cheat on the ends by replacing them with their neighbors\n",
    "                    psd_power[0] = psd_power[2]  # CHEAT!\n",
    "                    psd_power[1] = psd_power[2]  # CHEAT!\n",
    "                    psd_power[NFFT-1] = psd_power[NFFT-2]  # CHEAT!\n",
    "                \n",
    "                # Now copy the frequencies and powers into the long power spectrum around this particular center frequency\n",
    "                f = value*NFREQS + i # the starting NFFT-length chunk number\n",
    "                spectrum_lock.acquire()\n",
    "                #print('Writing freqs and powers chan', chan, 'center_freq', center_freq, 'psd_power[0]', psd_power[0])\n",
    "                spectrum_results['freqs'][f*NFFT:(f+1)*NFFT] = psd_frequencies + center_freq\n",
    "                spectrum_results['powers'][f*NFFT:(f+1)*NFFT] = psd_power\n",
    "                #is_interesting = this_is_interesting_and_we_should_record()\n",
    "                spectrum_lock.release()\n",
    "                #if is_interesting:\n",
    "                #    pass\n",
    "                #else:\n",
    "                #    # Stop Stream\n",
    "                #    stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)\n",
    "                #    streamer.issue_stream_cmd(stream_cmd)\n",
    "                \n",
    "        if task=='record':\n",
    "            # we might reach here from the top or because a spectral bin was interesting enough to keep recording\n",
    "            center_freq = value\n",
    "            \n",
    "            # set up stream command.\n",
    "            stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.num_done) # stream num_samps and then done\n",
    "            #stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.start_cont) # stream continuous\n",
    "            stream_cmd.stream_now = True\n",
    "            stream_cmd.num_samps = record_num_samps\n",
    "            \n",
    "            usrp.set_rx_freq(uhd.libpyuhd.types.tune_request(center_freq), chan=chan)\n",
    "            while not usrp.get_rx_sensor('lo_locked', chan).value:\n",
    "                # Maybe do processing from the previous step before or while we wait\n",
    "                time.sleep(0.01)\n",
    "            \n",
    "            time.sleep(0.02)  # Wait just for things to settle, but not long for X310\n",
    "            #time.sleep(0.10)  # really long time to test laptop issues\n",
    "            # Start Stream\n",
    "            streamer.issue_stream_cmd(stream_cmd)  # start the stream\n",
    "            # record time for the filename or SigMF data\n",
    "            start_time = datetime.datetime.now(datetime.timezone.utc)  # this is from the computer\n",
    "            # TODO: look at metadata that gets filled in below for a possibly better timestamp from the USRP\n",
    "            # Receive Samples\n",
    "            streamer.recv(record_recv_buffer, metadata)\n",
    "            #print(f'rx_thread_record chan={chan}  recv_buffer:', recv_buffer[-5:])\n",
    "            #print(f'rx_thread_record chan={chan}  metadata:', metadata)\n",
    "            # TODO: check if power is still there and we should still be recording\n",
    "            '''\n",
    "            # Stop continuous streaming if that's what we're doing\n",
    "            stream_cmd = uhd.types.StreamCMD(uhd.types.StreamMode.stop_cont)\n",
    "            streamer.issue_stream_cmd(stream_cmd)\n",
    "            '''\n",
    "            # Currently inspectrum can only read files with interleaved (complex) 32-bit floats,\n",
    "            timestring = start_time.strftime(\"%Y%m%d_%H%M%SZ\")  # UTC time, so mark it with a Z\n",
    "            output_file = f\"record_{timestring}_chan_{chan}_center_freq_{center_freq}_gain_{gain}_sample_rate_{sample_rate}_complex64.bin\"\n",
    "            with open(output_file, 'wb') as out_file:\n",
    "                if False: #args.numpy:\n",
    "                    np.save(out_file, record_recv_buffer, allow_pickle=False, fix_imports=False)\n",
    "                else:\n",
    "                    record_recv_buffer.tofile(out_file)\n",
    "        #time.sleep(0.5) # just to slow things down for debug\n",
    "    # we got a quit signal\n",
    "    print('ending empowered_rx_thread')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aaa8bcec-cc69-4d3a-995f-7ebbfb158f3f",
   "metadata": {},
   "outputs": [],
   "source": [
    "usrp = uhd.usrp.MultiUSRP(\"type=x300\")  # let driver pick size automatically. If MTU and buffer size are NOT set properly, it will suggest things.\n",
    "#usrp = uhd.usrp.MultiUSRP(\"type=x300,recv_frame_size=9000\") # UHD recommends a send frame size of at least 8000 for best performance\n",
    "#usrp = uhd.usrp.MultiUSRP(\"type=x300,num_recv_frames=1000\")  # if you are trying to receive at a high rate but are getting overflows, make the receive buffer much larger (the default value is 32)\n",
    "\n",
    "# TODO: Lock time to GPSDO \n",
    "# See https://files.ettus.com/manual/page_gpsdo_x3x0.html\n",
    "# and example https://github.com/EttusResearch/uhd/blob/master/host/examples/sync_to_gps.cpp\n",
    "# try running /usr/lib/uhd/examples/sync_to_gps --args \"type=x300\"\n",
    "\n",
    "#sample_rate = 100e6 # Hz max X310 but filter rolloff\n",
    "sample_rate = 50e6 # Hz for X310 with less rolloff\n",
    "gain = 50 # dB for X310 TwinRX\n",
    "#gain = 99 # dB max for X310 TwinRX\n",
    "\n",
    "# Make the list of center_freqs for the big global spectrum\n",
    "lo_freq_range = usrp.get_rx_freq_range()  # Check what our radio is capable of\n",
    "if lo_freq_range.start() > sample_rate/2:  # lowest RX frequency is significantly above zero, Like the B210\n",
    "    center_freqs = np.arange(lo_freq_range.start(), lo_freq_range.stop()+sample_rate, sample_rate)\n",
    "else:  # lowest RX frequency is close to zero. Like X310\n",
    "    #center_freqs = np.arange(sample_rate/2, lo_freq_range.stop(), sample_rate)\n",
    "    center_freqs = np.arange(sample_rate, lo_freq_range.stop(), sample_rate)  # start higher, offset by sample_rate/2\n",
    "    #center_freqs = np.arange(sample_rate/2*3/2, lo_freq_range.stop(), sample_rate)  # offset by a half-spacing. ends ok now\n",
    "print('Number of center_freqs:', len(center_freqs), 'for', \n",
    "          lo_freq_range.start()/1e6, 'MHz to', \n",
    "          lo_freq_range.stop()/1e6, 'MHz at',\n",
    "          sample_rate/1e6, 'MSPS')\n",
    "\n",
    "# parameters for the big global spectrum\n",
    "NFFT = 256 # original testing here\n",
    "#NFFT = 1024\n",
    "freqs = np.zeros(len(center_freqs)*NFFT)  # frequencies for each small spectral bin of large spectrum. TODO: if there is overlap, change this\n",
    "spectrum_results = {  # access this dictionary ONLY between spectrum_lock.acquire() and spectrum_lock.release()\n",
    "    # Some global shared info that shouldn't change\n",
    "    'sample_rate'      : sample_rate,\n",
    "    'gain'             : gain, # maybe this can be changed per-channel based on signal strength\n",
    "    'NFFT'             : NFFT,\n",
    "    'center_freqs'     : center_freqs,\n",
    "    'freqs'            : freqs,\n",
    "    # spectrum threads update this:\n",
    "    'powers'           : np.zeros(len(freqs))+1e-200,  # Before we go take data, be sure not to find any interesting power\n",
    "    'psd_time'         : np.zeros(len(freqs)),  # how long did it take to record these channels in unix timestamp\n",
    "    'timestamps'       : np.zeros(len(freqs)),  # times when each spectral bin was last recorded\n",
    "    'by_chan'          : np.zeros(len(freqs)),  # which channel recorded each spectral bin\n",
    "    # things for the threads to coordinate their next action:\n",
    "    'what_channel_is_working_on' : [('idle',None)]*usrp.get_rx_num_channels(),  # list of ('task', value) pairs. task is 'record' or 'spectrum'\n",
    "    'next_spectral_range_index'  : 0,  # goes 0...NUM_CHANNELS-1.  If a channel should take a spectrum, do this piece.\n",
    "    'trigger_power_dB' : {},  # a map from trigger frequency to trigger power. For plotting.\n",
    "}\n",
    "#recording_results = {}  # use this without locks, so don't access it from the main thread until the worker has joined\n",
    "\n",
    "# threads inspired by the benchmark_rate.py example...\n",
    "\n",
    "threads = []\n",
    "quit_event = threading.Event()  # a signal for the threads to stop running\n",
    "spectrum_lock = threading.Lock() # a lock to share spectrum data\n",
    "\n",
    "\n",
    "# For now, set all 4 channels to the same parameters\n",
    "for chan in range(usrp.get_rx_num_channels()):\n",
    "    usrp.set_rx_rate(sample_rate, chan=chan)\n",
    "    usrp.set_rx_gain(gain, chan=chan)\n",
    "    st_args = uhd.usrp.StreamArgs(\"fc32\", \"sc16\")  # complex float32 in CPU, signed int16 over the wire\n",
    "    st_args.channels = [chan]\n",
    "    streamer = usrp.get_rx_stream(st_args)\n",
    "    thread = threading.Thread(target=empowered_rx_thread,\n",
    "                  args=(usrp, streamer, chan, spectrum_results, spectrum_lock, quit_event))\n",
    "    thread.name = f\"thread_chan{chan}\"\n",
    "    thread.start()\n",
    "    threads.append(thread)\n",
    "\n",
    "while np.any([t.is_alive() for t in threads]):  # while any thread is still alive\n",
    "    spectrum_lock.acquire()\n",
    "    # Update the Bokeh spectrum plot above. \n",
    "    # Math is done here, so we don't have to worry about the data changing after we release the lock\n",
    "    source.data = {\n",
    "        \"freqs_MHz\" : spectrum_results['freqs']/1e6, # x\n",
    "        \"powers_dB\" : 10*np.log10(spectrum_results['powers']), # y\n",
    "    }\n",
    "    marker_freq = np.array([\n",
    "                center_freq for (task, center_freq) \n",
    "                       in spectrum_results['what_channel_is_working_on'] \n",
    "                           if task=='record'])\n",
    "    marker_power = [spectrum_results['trigger_power_dB'][freq] for freq in marker_freq]\n",
    "    source_marker.data = {\n",
    "        \"marker_MHz\" : marker_freq/1e6, # x\n",
    "        \"marker_dB\"  : marker_power, # y\n",
    "    }\n",
    "    spectrum_lock.release()\n",
    "    push_notebook(handle=handle)\n",
    "    time.sleep(1/15) # for a rough number of frames per second FPS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8bd518f6-0dbc-48ad-9122-ee20fa53c6bf",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e83b1d89-f374-47dc-b31b-ecb0dc343073",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b34cd1df-9ac0-4ce7-b088-3100fe875ae2",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "22067996-a4b3-42e7-8802-6718c6c3f17e",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "c25ea774-0e9e-411c-a428-10919b6b20d7",
   "metadata": {},
   "source": [
    "# Plot a recorded signal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d64069f2-ecf6-4048-badc-671fbac67c29",
   "metadata": {},
   "outputs": [],
   "source": [
    "!ls record*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "170f210b-a0d3-4f06-b1a6-02bfd39d044a",
   "metadata": {},
   "outputs": [],
   "source": [
    "filename = 'record_20240406_045943Z_chan_0_center_freq_2464257812.5_gain_50_sample_rate_50000000.0_complex64.bin'\n",
    "a = np.fromfile(filename, dtype=np.complex64)\n",
    "a = a[:200*1000]  # take the first 4ms"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e98c6455-045a-4dc0-9111-11700afaf84b",
   "metadata": {},
   "outputs": [],
   "source": [
    "p = figure(\n",
    "        height=400,\n",
    "        title='amplitude',\n",
    "        x_axis_label='time [ms]',\n",
    "        y_axis_label='amplitude',\n",
    "        tools=(\"pan\", \"zoom_in\", \"zoom_out\", \"box_zoom\", \"wheel_zoom\", \"save\", \"reset\", \"help\", \"hover\"),\n",
    "        sizing_mode=\"stretch_width\",\n",
    "    )\n",
    "time = np.arange(len(a))/sample_rate*1e3 # ms\n",
    "p.line(x=time, y=a.real, alpha=0.5)\n",
    "p.line(x=time, y=a.imag, alpha=0.3, color='red')\n",
    "show(p)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4b8ac73a-ef15-47a6-97b1-6e6e884f82c3",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b10a1674-27b4-4453-b715-69c909fdc268",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4c23110a-39bb-4b52-8876-4f979950e40e",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2232dd84-8afe-4bba-ae30-f14ba8b5c733",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b8f69bc8-ceb7-4f2a-acc3-9742cb917c80",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "299b0061-5d3e-4021-a479-8d440542d79a",
   "metadata": {},
   "source": [
    "### TODO\n",
    "\n",
    "* Record the time that a bin crossed the threshold and the time streaming started. Laptop time and UHD time\n",
    "* SigMF metadata with int16 unfiltered output recording. float32 for inspectrum. \n",
    "* A list of data recording plots that can be added to. Demod the button or Walkie Talkie?\n",
    "* Message passing instead of shared dictionary to allow many X310 boxes on different computers?\n",
    "* Stream continuously when recording and have a stop criteria.\n",
    "* What happens if we are taking spectra and get a bright chunk?\n",
    "    - Go onto the next chunk of spectra or switch to recording mode, saving the data that triggered this?\n",
    "    - Maybe just keep recording in that channel and stop somewhere else if it's lower priority. Continuous vs N samples?\n",
    "    - Check if any samples are lost when switching from streaming num_done (N samples and then stop) to start_cont (continuous)\n",
    "* Conversely, while it's recording, it could be contributing to the global spectrum. You have to start centered on a spectral bin.\n",
    "* Demo with key fob buttons, walkie talkie, RFM69, Lora burst, Drone TX, B210 ZC random\n",
    "* Maybe spectrum takes Pseudorandom hops to get spectral chunks at the cost of PLL lock time?\n",
    "* RFNoC FFT for X310?\n",
    "* Can you continuously stream on one channel in one thread while changing carrier and toggling streaming with the other? Yes. Sample rate? Not sure.\n",
    "* Special channels connected to different antennas?\n",
    "* Gain for each channel?\n",
    "* Gain set for recording based on signal strength?\n",
    "* Overlap the taking of spectra to not cheat in the middle and edge bins\n",
    "* Calibrate spectra to physics dBm or at least flat across each sub-band"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e456381b-3093-45b0-9892-2fdb11852f72",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
